返回文章列表

Redis 过期删除和内存淘汰:理解 key 生命周期,别把"过期"和"被赶走"混为一谈

很多 Redis 线上问题,最后都会回到一个很朴素的问题:

Redis 过期删除和内存淘汰:理解 key 生命周期,别把”过期”和”被赶走”混为一谈

很多 Redis 线上问题,最后都会回到一个很朴素的问题:

这个 key 为什么没了?或者,为什么它该没的时候还没没?

商品详情缓存设置了 5 分钟 TTL,5 分钟后内存为什么没有马上下降?购物车没有设置过期时间,为什么内存满时写入失败?某些缓存还没过期,为什么突然查不到了?大量 key 同一秒过期,为什么 Redis 延迟突然抖了一下?

这些问题背后其实是两套规则在同时工作:过期删除决定 key 到点后还算不算有效,内存淘汰决定内存顶满时该牺牲谁。后面继续沿用前文的电商系统例子,把商品缓存、session、购物车、排行榜和库存计数都放进同一条 key 生命周期里来看。

写作前先把主线摆出来:

原来:Redis 只是把 key 放进内存,读写都很快
问题 1:缓存不能永远有效,否则会越来越脏、越来越占空间
机制 1:TTL 和过期删除出现,用时间决定 key 还该不该活
问题 2:过期不等于立刻释放内存,删除本身也会带来成本
机制 2:惰性删除、定期删除、异步释放一起平衡及时性和性能
问题 3:即使没到过期时间,内存也可能装不下
机制 3:maxmemory 和淘汰策略出现,用容量压力决定牺牲谁
Redis key 生命周期

先看这张生命周期图。它最重要的提醒只有一句话:TTL 表示逻辑过期,真正删除往往发生在后面的访问或后台扫描里。

一、先把一个 key 的生命走一遍

我们给商品详情缓存设置 5 分钟 TTL:

SET product:10086 "{...}" EX 300

从业务角度看,我们会说:“这个 key 5 分钟后过期。”

但从 Redis 角度看,更准确的是:Redis 保存了两份信息。一份是正常的 key-value 数据,另一份是这个 key 对应的过期时间。到了这个时间点之后,这个 key 在逻辑上不应该再被当作有效数据。

注意这里有个很关键的分界:

  • 逻辑过期:时间到了,这个 key 不该再返回给业务;
  • 物理删除:Redis 真的把 key 和 value 从内存结构里删掉;
  • 内存归还:对象释放后,内存分配器是否把内存还给操作系统。

很多误会就发生在这里。我们以为 TTL 到点就等于”内存马上下降”,但 Redis 并不这么承诺。TTL 控制的是有效性,不是精确的内存回收时钟。

04 过期删除和内存淘汰 图 1

所以第一条记忆是:

TTL 表示逻辑过期时间,不表示物理删除必须精确发生在那一刻。

二、过期删除回答的是”这个 key 还该不该活”

Redis 通常结合两种方式处理过期 key:惰性删除定期删除

惰性删除很直观:下次访问这个 key 时,Redis 先检查它是否过期。如果过期,就顺手删除并返回不存在。它的好处是省事,不访问就不花检查成本;坏处是,如果一个过期 key 再也没人访问,它可能会在内存里多待一会儿。

定期删除则是 Redis 周期性抽样一部分带过期时间的 key,检查哪些已经过期,然后删除一批。它的好处是能主动清理冷 key;坏处是不能每次都全量扫描,否则 Redis 会把大量时间花在”打扫卫生”上,而不是处理业务命令。

为什么不在过期瞬间立刻删除所有 key?

因为 Redis 不可能为每个 key 都安排一个精确闹钟。一个电商活动可能一次性写入几十万、几百万个缓存,如果这些 key 同一秒到期,过期瞬间全部删除会让主线程被删除工作拖住。Redis 选择用”访问时顺手删 + 后台抽样删”的方式,在及时性和性能之间折中。

官方文档里还有一个容易忽略的边界:过期事件本身也不是严格的定时器事件。一个 key 到期后,只有当它被访问、被主动扫描发现,或者被其他命令删除时,Redis 才真正产生相应的删除动作。也就是说,业务上不要把 TTL 当成毫秒级调度器。

在电商系统里,TTL 适合表达”这份缓存多久后不值得信任”:商品详情缓存 5 到 30 分钟,验证码 5 分钟,分布式锁 10 秒,活动预热数据到活动结束后失效。TTL 不适合表达”第 300 秒必须执行某个业务动作”。后者应该交给任务调度、消息队列或专门的延迟任务系统。

三、为什么设置了 TTL,内存没有立刻下降

假设活动结束后,10 万个商品缓存都过期了。你看业务日志,发现这些 key 已经过期;但看 Redis 进程内存,占用并没有立刻下降。

这可能不是 bug。

原因至少有四类。

第一,过期 key 还没被扫描到。惰性删除要等访问,定期删除要等抽样。没有被访问、也没被抽样命中的 key,可能短时间还留在内存里。

第二,Redis 删除对象后,内存分配器未必马上把内存归还给操作系统。进程内可复用空间变多了,但操作系统看到的 RSS 不一定马上下降。

第三,大量删除可能造成内存碎片。业务对象释放了,但空洞不连续,进程占用看起来仍然偏高。这个时候看 used_memoryused_memory_rssmem_fragmentation_ratio,比只盯着操作系统层面的进程内存更有意义。

第四,设置 TTL 本身也有元数据成本。Redis 需要为带过期时间的 key 维护额外的过期字典信息。也就是说,不是”所有 key 都加 TTL 就一定更省内存”。TTL 解决的是生命周期问题,不是免费的空间魔法。

所以排查内存时,不能只问”key 是否过期”,还要问:

  • 它有没有被访问或扫描到;
  • 删除动作有没有集中发生;
  • value 是不是大对象;
  • 内存分配器有没有把空间还给操作系统;
  • 业务是不是把大量低价值数据也写进了 Redis。

对电商系统来说,给商品缓存设置 TTL 是必要的,但 TTL 不是内存治理的全部。你还需要设计 key 上限、过期时间分布、大 key 拆分和监控指标。

四、批量过期为什么会制造延迟毛刺

缓存雪崩里常说”过期时间加随机值”。这不是仪式感,而是为了避免过期删除压力集中爆发。

假设你在 10:00 批量预热 100 万个商品缓存,TTL 都是 30 分钟。那么 10:30 之后,大量 key 同时进入过期状态。Redis 的定期删除会在短时间里扫描并删除更多过期 key,业务请求也会不断触发惰性删除。删除本身需要释放对象,如果对象比较大,成本还会放大。

结果就是 Redis 主线程的正常命令处理被删除工作夹住,出现延迟抖动。

04 过期删除和内存淘汰 图 2

解决办法很朴素:

  • TTL 加随机扰动,让过期时间分散;
  • 热点 key 后台刷新,不让它们自然集中失效;
  • 大 key 拆分,避免删除单个 key 太重;
  • 使用 UNLINK 或 lazyfree 相关能力处理大对象释放;
  • 对活动预热数据设置分批加载和分批失效。

DEL 更像”现在就把对象释放掉”,而 UNLINK 更像”先把 key 从 keyspace 摘掉,释放内存的重活交给后台做”。对于大 List、大 Hash、大 ZSet 这类对象,异步释放能降低主线程被释放成本拖住的风险。它不是让删除没有成本,而是把一部分成本挪出命令处理路径。

这类问题最容易被误判成”Redis 性能突然变差”。实际上,它是 key 生命周期设计不均匀造成的。

五、内存淘汰回答的是”内存不够时牺牲谁”

过期删除处理的是生命周期。内存淘汰处理的是容量压力。

Redis 内存淘汰策略

这张图对应的是另一套问题链:不是“它过没过期”,而是“内存满了以后谁先走”。

假设 Redis 设置了 maxmemory。当写入新数据导致内存超过限制时,Redis 要决定怎么办。

这时淘汰策略开始发挥作用。

04 过期删除和内存淘汰 图 3

如果策略是 noeviction,Redis 不主动淘汰 key,而是让写命令失败。这适合 Redis 里存放比较重要、不能随便丢的数据。例如你把某些幂等标记或关键状态放进 Redis,就不希望它们因为内存满被悄悄淘汰。

如果策略是 allkeys-lru,Redis 会在所有 key 里选择最近最少使用的 key 淘汰。这更像标准缓存:谁不常用,谁先走。

如果策略是 allkeys-lfu,Redis 会根据访问频率淘汰不常访问的 key。它更关注”长期用得少”,而不只是”最近没用”。

如果策略是 volatile-lruvolatile-lfuvolatile-ttl 等,只在设置了 TTL 的 key 里选择淘汰对象。这适合你把 Redis 分成两类:设置 TTL 的是缓存,可以牺牲;没设置 TTL 的是更重要的状态,不希望被淘汰。

volatile-* 有一个重要边界:如果没有足够的带 TTL 的 key 可淘汰,Redis 仍然可能无法为写入腾出空间,表现会接近 noeviction。所以选择 volatile-* 的前提是:你的可牺牲数据真的都设置了 TTL,而且数量足够。

第二条记忆是:

过期删除看时间,内存淘汰看容量。过期 key 是该不该活,淘汰策略是装不下时谁先走。

六、为什么”缓存 key”和”状态 key”最好不要混在一起

电商系统里,Redis 可能同时存这些数据:

  • 商品详情缓存:丢了可以回源 MySQL;
  • 用户 session:丢了用户要重新登录;
  • 购物车:如果只存在 Redis,丢了会影响订单;
  • 排行榜:可重建但成本较高;
  • 分布式锁:必须短 TTL,不允许长期存在;
  • 幂等 key:短时间内很重要。

如果这些 key 混在同一个 Redis 实例里,再配置一个激进的 allkeys-lru,内存满时 Redis 可能把 session、幂等 key、排行榜数据也淘汰掉。系统表面上只是”缓存满了”,实际可能变成业务事故。

这就是淘汰策略的本质:它不是技术参数,而是在选择牺牲面。

比较稳妥的做法是按数据等级分层:

  • 纯缓存数据放在一个实例或独立集群,允许 LRU/LFU 淘汰;
  • 关键状态尽量不要和普通缓存混用;
  • 必须混用时,借助 TTL、key 前缀、容量隔离和监控降低风险;
  • 对不能丢的数据,不要只依赖 Redis 内存状态。

所以 Redis key 生命周期设计,不只是给每个 key 起名字,还要决定它能不能过期、能不能被淘汰、丢失后怎么恢复。

可以把常见数据粗略分成三层:

数据类型能不能过期能不能被淘汰丢了怎么办推荐思路
商品详情缓存通常能回源数据库TTL 加随机值,允许 allkeys-lruallkeys-lfu
用户 session谨慎用户重新登录或走恢复逻辑独立实例或保守策略,配合续期
购物车看设计谨慎如果无落库会丢业务数据Redis 只做加速层更稳
分布式锁必须能不靠淘汰锁语义可能被破坏短 TTL、owner 校验、不要靠淘汰释放
排行榜可分期看重建成本从日志或数据库重建分片、归档、限制集合增长
签到 Bitmap可按周期看业务要求历史统计可能缺口按月或按年建 key,设置保留期

七、淘汰策略会反过来影响数据库压力

内存淘汰不是 Redis 内部小事,它会影响整个系统链路。

商品详情缓存被淘汰后,下一次请求会回源数据库。如果淘汰策略和业务访问模式不匹配,大量有价值的热点 key 被淘汰,缓存命中率下降,数据库压力就会上升。

更麻烦的是,这种压力可能形成循环:

04 过期删除和内存淘汰 图 4

所以看 Redis 内存不能只看”有没有超过 maxmemory”。还要看:

  • keyspace_hits / keyspace_misses 推出的 hit rate 是否下降;
  • evicted_keys 是否持续增长;
  • expired_keys 是否集中跳变;
  • used_memoryused_memory_rssmem_fragmentation_ratio 是否异常;
  • 是否存在 big key 占用过多内存;
  • 是否存在缓存污染,把不常访问的数据塞满了 Redis。

缓存污染是一个很实际的问题。比如一次批处理把大量冷门商品也写进 Redis,挤掉真正热门商品。表面上 Redis 里 key 很多,实际命中率反而变差。

如果业务访问有明显热点,allkeys-lfu 往往比单纯 LRU 更能抵抗”一次性冷数据扫描”。如果业务访问具有很强的最近性,比如用户刚看过的商品短期内可能继续看,LRU 就更自然。策略选择不是背配置表,而是把访问模式和牺牲面讲清楚。

八、排查时先问三件事

遇到”key 怎么没了”或”内存怎么还没降”,不要一上来就猜 Redis 异常。可以按三步问。

第一,key 是过期没了,还是被淘汰没了?

TTL keyPTTL key 只能判断当前状态,事后还要结合 expired_keysevicted_keys、应用日志和 key 的写入策略。expired_keys 增长,说明过期删除在发生;evicted_keys 增长,说明 maxmemory 压力下已经开始淘汰。

第二,删除是集中发生,还是平滑发生?

如果某个时间点 expired_keys 突然跳变,同时延迟升高,就要怀疑同秒批量过期、大 key 删除或缓存雪崩。如果 evicted_keys 持续增长,同时 hit rate 下降,就要怀疑内存容量不足、策略不匹配或缓存污染。

第三,内存没降是真的没删,还是删了但 RSS 没降?

如果 key 数下降、used_memory 下降,但 used_memory_rss 不降,更多是分配器和碎片问题。如果 key 数和 used_memory 都没怎么变,那才要继续查过期扫描、写入速度、大 key 和业务回填。

可以记一张小排障表:

现象优先怀疑关键指标
TTL 到了但 key 短时间还占内存惰性删除和定期删除尚未触达TTL、key 数、expired_keys
key 没过期却消失maxmemory 淘汰evicted_keysmaxmemory-policy
内存满后写入失败noeviction 或无候选 key错误日志、maxmemory-policy
删除后 RSS 不降内存碎片或分配器未归还used_memory_rssmem_fragmentation_ratio
延迟周期性抖动批量过期、大 key 删除LATENCY DOCTOR、慢日志、expired_keys 跳变
DB 压力升高命中率下降、热点被淘汰hit rate、evicted_keys、DB QPS

九、怎么为不同业务设计 key 生命周期

最后回到电商系统,给几个常见数据定规则。

商品详情缓存:可以设置 TTL,比如 5 到 30 分钟,并加随机扰动。更新商品时,先更新数据库再删除缓存。热点商品可以后台刷新。

用户 session:TTL 和登录态绑定,通常要滑动续期。它不应该和普通商品缓存一起被随意淘汰。

购物车:如果 Redis 只是加速层,最终数据应落数据库;如果 Redis 是主存储,就必须有持久化、备份和更保守淘汰策略。

分布式锁:必须有较短 TTL,释放时校验 owner,不能依赖淘汰策略解决锁泄露。

排行榜:可设置周期性归档和分片,不要让一个 ZSet 无限增长。是否允许淘汰,要看是否能从业务日志重建。

签到 Bitmap:按月或按年建 key,历史数据设置合理保留期,避免永久增长。

这时你会发现,Redis key 生命周期不是一个统一答案,而是每类数据各自的生命周期设计。

真正落地时,可以按下面这条清单过一遍:

  • 这个 key 是缓存、状态,还是临时控制标记?
  • 它的真相源在哪里,MySQL、消息日志,还是 Redis 自己?
  • 它能不能过期,过期后谁负责重建?
  • 它能不能被淘汰,被淘汰后业务是否能接受?
  • 它的 value 会不会变成大 key,删除成本多高?
  • 它的 TTL 会不会和一批 key 集中到同一秒?
  • 它应该和哪些 key 放在同一个实例,和哪些 key 隔离?

十、总结:key 消失有两种原因

过期删除和内存淘汰最容易混淆,可以用一句话分开:

过期删除是时间到了,内存淘汰是位置不够了。

过期删除不保证精确时刻物理删除,它通过惰性删除和定期删除在及时性和成本之间平衡。TTL 让 key 在逻辑上失效,但物理删除和内存归还会受访问、扫描、对象大小、分配器和碎片影响。

内存淘汰只在容量触顶时介入,它按照策略选择牺牲对象。不同策略对应不同业务假设,没有绝对最好。allkeys-* 适合把 Redis 当纯缓存,volatile-* 适合只淘汰带 TTL 的缓存层,noeviction 则宁可写失败也不悄悄丢数据。

真正的工程问题,是把 key 分成不同等级:哪些能过期,哪些能被淘汰,哪些丢了能重建,哪些必须落到更可靠的系统。

理解 key 生命周期后,你再看缓存雪崩、缓存污染、大 key 删除、内存碎片、命中率下降,就会发现它们不是孤立问题,而是同一件事的不同侧面:

Redis 很快,但内存有限;key 的生老病死如果设计不好,快就会变成抖,缓存就会变成事故源。